#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import os
import re
from base64 import b64decode

from common.file_common import change_path_permission
from common.util.exec_utils import su_exec_rm_cmd
from redis import log

from redis.common.parse_param import get_env_value, redis_parse_param_with_jsonschema

from common.const import AuthType
from common.common import output_result_file

from redis.service.redis_client import RedisClient
from redis.schemas.event_message import CommonBodyErrorResponse, \
    QueryClusterResponse, ListAppResourceInfo, ListAppV2Response, RedisClusterNodeInfo
from redis.schemas.redis_info_schema import RedisNodeInfo, RedisNodeExtendInfo, Auth
from redis.common.const import NodeRole, AccessPermission, \
    RedisNodeKeyConstant, KerberosConstant, ErrorCode, RedisPluginPathConstant


class ClusterResManager:
    BIND_ALL = '0.0.0.0'
    LOOPBACK_ADDRESS = '127.0.0.1'
    RESULT_FILE_PREFIX = 'result'
    PROGRESS_SPLIT_LEN = 3
    SLAVE_REG = '^slave[0-9]+'
    CONNECTED_FLAG = " connected "

    @staticmethod
    def query_cluster(pid):
        log.info('start to query cluster')
        redis_nodes = []
        tmp_files = []
        try:
            file_content = redis_parse_param_with_jsonschema(pid)
            extend_info = file_content['application']['extendInfo']
            auth_obj, tmp_files = ClusterResManager.pre_auth_job(
                pid, file_content['application']['auth'], 'application_auth')
            extend_ip = extend_info['ip']
            extend_port = extend_info['port']
            log.info(f'query cluster, ip: {extend_ip}, port: {extend_port}')
            user, cmd_path, service_ip = ClusterResManager.get_redis_process_info(extend_ip, extend_port)
            cluster_info = RedisClient.query_redis_cluster_all_nodes(user, cmd_path, auth_obj, extend_info)
            for info in cluster_info:
                log.info(f'cluster info: {info}')
                # 去除不匹配的行
                if NodeRole.MASTER.name.lower() not in info and NodeRole.SLAVE.name.lower() not in info:
                    continue
                node = ClusterResManager.build_redis_node_by_cluster_node(info)
                redis_nodes.append(node)
        except Exception as err:
            log.error(err, exc_info=True)
        response = QueryClusterResponse()
        nodes = []
        for info in redis_nodes:
            res_extend = RedisNodeExtendInfo(ip=info.ip, port=info.port,
                                             slot=info.slot, role=info.role).dict(by_alias=True)
            nodes.append(RedisClusterNodeInfo(id=info.uuid, extendInfo=res_extend).dict(by_alias=True))
        response.nodes = nodes
        output_result_file(pid, response.dict(by_alias=True))
        ClusterResManager.delete_files(tmp_files)

    @staticmethod
    def check_application(pid):
        log.info('start to check application')
        response = CommonBodyErrorResponse()
        tmp_files = []
        try:
            file_content = redis_parse_param_with_jsonschema(pid)
            extend_info = file_content['application']['extendInfo']
            extend_ip = extend_info['ip']
            extend_port = extend_info['port']
            user, cmd_path, service_ip = ClusterResManager.get_redis_process_info(extend_ip, extend_port)
            log.info(f'pid: {pid}, user: {user}, cmd_path: {cmd_path}, ip: {service_ip}')
            auth_obj, tmp_files = ClusterResManager.pre_auth_job(
                pid, file_content['application']['auth'], 'application_auth')
            redis_node_dir = RedisClient.query_redis_node_dir(user, cmd_path, auth_obj, extend_info)
            if len(redis_node_dir) == 0:
                response.code = ErrorCode.ERROR_NETWORK_CONNECT_TIMEOUT.value
                response.body_err = ErrorCode.ERROR_NETWORK_CONNECT_TIMEOUT.value
        except Exception as err:
            log.error(err, exc_info=True)
            response.code = ErrorCode.ERROR_NETWORK_CONNECT_TIMEOUT.value
            response.body_err = ErrorCode.ERROR_NETWORK_CONNECT_TIMEOUT.value
        finally:
            output_result_file(pid, response.dict(by_alias=True))
            ClusterResManager.delete_files(tmp_files)

    @staticmethod
    def list_application_v2(pid):
        log.info('start to list application v2')
        tmp_files = []
        response = ListAppV2Response()
        try:
            file_content = redis_parse_param_with_jsonschema(pid)
            applications = file_content['applications']
            if len(applications) != 1:
                raise Exception(f'application param error')
            auth_obj, tmp_files = ClusterResManager.pre_auth_job(pid, applications[0]['auth'], 'applications_0_auth')
            extend_info = applications[0]['extendInfo']
            extend_ip = extend_info['ip']
            extend_port = extend_info['port']
            user, cmd_path, service_ip = ClusterResManager.get_redis_process_info(extend_ip, extend_port)
            redis_node_info = RedisClient.query_redis_cluster_node_info(user, cmd_path, auth_obj, extend_info)
            ClusterResManager.assert_empty_result(redis_node_info)
            cluster_info = RedisClient.query_redis_cluster_all_nodes(user, cmd_path, auth_obj, extend_info)
            ClusterResManager.assert_empty_result(cluster_info)
            redis_dir = RedisClient.query_redis_node_dir(user, cmd_path, auth_obj, extend_info)
            ClusterResManager.assert_empty_result(redis_dir)
            redis_dbfilename = RedisClient.query_redis_node_dbfilename(user, cmd_path, auth_obj, extend_info)
            ClusterResManager.assert_empty_result(redis_dbfilename)
            slot = ClusterResManager.get_redis_slot_from_cluster_nodes(cluster_info, service_ip, extend_port)
            all_info = ClusterResManager.build_redis_all_info(redis_node_info, slot, redis_dir, redis_dbfilename)
            response.resource_list = all_info
            response.total = 1
        except Exception as err:
            log.error(err, exc_info=True)
            resource_info = ClusterResManager.get_offline_node_application_info()
            response.resource_list = resource_info
            response.total = 1
        finally:
            log.info(f'list application v2 response: {response}')
            output_result_file(pid, response.dict(by_alias=True))
            ClusterResManager.delete_files(tmp_files)

    @staticmethod
    def get_offline_node_application_info():
        app_info = []
        info = ListAppResourceInfo(extendInfo={'pair': ''}).dict(by_alias=True)
        app_info.append(info)
        return app_info

    @staticmethod
    def get_redis_process_info(extend_ip, port):
        progresses = RedisClient.query_redis_cluster_process()
        service_ip = extend_ip
        for progress in progresses:
            cmd_items = progress[2].split(' ')
            if len(cmd_items) < ClusterResManager.PROGRESS_SPLIT_LEN:
                continue
            # 忽略不包含指定端口号的进程
            host = cmd_items[-2].strip()
            host_info = host.split(':')
            if str(port) != str(host_info[-1]):
                continue
            log.info(f'redis process: {host}')
            # 如果绑定的是*，用127.0.0.1连接
            if extend_ip is ClusterResManager.BIND_ALL:
                service_ip = ClusterResManager.LOOPBACK_ADDRESS
            return progress[0], progress[1].rsplit(os.path.sep, 1)[0], service_ip

    @staticmethod
    def build_redis_node_by_cluster_node(cluster_info):
        # 6e3c6c754a10dbcb ip:port@24350 myself,master - 0 1653899473000 3 connected 5461-10921
        # 6e3c6c754a10dbcb ip:port@24350 master,fail - 1653919396589 1653919394000 8 disconnected
        # b74cf2ec0d38 ip:port@24352 master - 0 1657678844264 13 connected 0-1090 10922-12014 13107-16383
        items = cluster_info.split(' ')
        # ip:端口
        host_info = items[1].split('@')[0]
        role = ClusterResManager.get_redis_role(items[2])
        slot = ''
        if role == NodeRole.MASTER.value:
            slot = ClusterResManager.get_slot_info_from_cluster_nodes(cluster_info)
        log.info(f'host_info: {host_info}, role: {role}, slot: {slot}')
        return RedisNodeInfo(
            uuid=items[0], ip=host_info.split(':')[0], port=host_info.split(':')[-1], role=role, slot=slot)

    @staticmethod
    def get_redis_slot_from_cluster_nodes(cluster_infos, service_ip, port):
        # 21cf2c112410a1 ip:端口@17001 myself,master - 0 1654742175000 1 connected 0-5460
        # f78629943d2f9e ip:端口@17002 master - 0 1654742174458 2 connected 5461-10922
        for info in cluster_infos:
            # 去除不匹配的行
            if NodeRole.MASTER.name.lower() not in info and NodeRole.SLAVE.name.lower() not in info:
                continue
            node = ClusterResManager.build_redis_node_by_cluster_node(info)
            if str(service_ip) == str(node.ip) and int(port) == int(node.port):
                return node.slot
        return ''

    @staticmethod
    def build_redis_all_info(items, slot, redis_dir, redis_dbfilename):
        app_info = []
        slaves = []
        pair = []
        # items存放着k:v格式的字符串，如：[redis_version:6.2.6,role:master]
        info_dicts = ClusterResManager.convert_redis_info_to_dict(items)
        version = info_dicts.get(RedisNodeKeyConstant.VERSION.value)
        role = ClusterResManager.get_redis_role(info_dicts.get(RedisNodeKeyConstant.ROLE.value))
        aof_enabled = int(info_dicts.get(RedisNodeKeyConstant.AOF_ENABLED.value))
        master_host = info_dicts.get(RedisNodeKeyConstant.MASTER_HOST.value)
        master_port = info_dicts.get(RedisNodeKeyConstant.MASTER_PORT.value)
        for key in info_dicts.keys():
            if re.match(ClusterResManager.SLAVE_REG, key):
                # 备节点格式：ip=***,port=***,state=online,offset=4667771,lag=1
                slave_ip, port = ClusterResManager.get_slave_ip_port(info_dicts.get(key))
                slaves.append(f'{slave_ip}:{port}')
        if role == NodeRole.SLAVE.value:
            pair.append(f'{master_host}:{master_port}')
        else:
            pair = slaves
        log.info(f'pair: {pair}')
        extend_info = {
            'version': version,
            'role': role,
            'slot': slot,
            'dir': redis_dir,
            'dbfilename': redis_dbfilename,
            'aofEnabled': aof_enabled,
            'pair': ','.join(pair)
        }
        info = ListAppResourceInfo(extendInfo=extend_info).dict(by_alias=True)
        app_info.append(info)
        return app_info

    @staticmethod
    def pre_auth_job(pid, auth, param_prefix):
        tmp_files = []
        # 构造认证信息
        auth_object = ClusterResManager.build_auth_object(pid, param_prefix, auth)
        # 如果是kerberos认证，需要将keytab\conf字符串写入文件保存
        if auth_object.auth_type is AuthType.KERBEROS.value:
            if auth_object.extend_info[KerberosConstant.KEYTAB.value] is not None:
                keytab_filename = f'user_{pid}.keytab'
                ClusterResManager.write_file(RedisPluginPathConstant.TMP_PATH.value, keytab_filename,
                                             b64decode(auth_object.extend_info[KerberosConstant.KEYTAB.value]), 'wb')
                keytab_filepath = os.path.join(RedisPluginPathConstant.TMP_PATH.value, keytab_filename)
                auth_object.extend_info[KerberosConstant.KEYTAB.value] = keytab_filepath
                tmp_files.append(keytab_filepath)
            conf_filename = f'krb5_{pid}.conf'
            ClusterResManager.write_file(RedisPluginPathConstant.TMP_PATH.value, conf_filename,
                                         b64decode(auth_object.extend_info[KerberosConstant.KRB5_CONF.value]), 'wb')
            conf_file_path = os.path.join(RedisPluginPathConstant.TMP_PATH.value, conf_filename)
            tmp_files.append(conf_file_path)
            auth_object.extend_info[KerberosConstant.KRB5_CONF.value] = conf_file_path
        return auth_object, tmp_files

    @staticmethod
    def build_auth_object(pid, env_prefix, auth):
        auth_object = Auth()
        auth_object.auth_type = int(get_env_value(f'{env_prefix}_authType_{pid}'))
        auth_object.auth_key = get_env_value(f'{env_prefix}_authKey_{pid}')
        auth_object.auth_pwd = get_env_value(f'{env_prefix}_authPwd_{pid}')
        auth_object.extend_info = auth['extendInfo']
        auth_object.extend_info[KerberosConstant.KEYTAB.value] = \
            get_env_value(f'{env_prefix}_extendInfo_keytab_{pid}')
        auth_object.extend_info[KerberosConstant.SECRET.value] = \
            get_env_value(f'{env_prefix}_extendInfo_secret_{pid}')
        return auth_object

    @staticmethod
    def write_file(parent_path, filename, content, mode):
        if not os.path.exists(parent_path):
            log.error(f'{parent_path} is not exist')
            raise Exception(f'{parent_path} is not exist')
        file_path = os.path.join(parent_path, filename)
        with open(file_path, mode) as file:
            file.write(content)
        change_path_permission(file_path, mode=AccessPermission.RWX_RX_RX.value)

    @staticmethod
    def delete_files(files: list):
        for file in files:
            su_exec_rm_cmd(file, check_white_black_list_flag=False)

    @staticmethod
    def get_redis_role(content):
        if NodeRole.MASTER.name in content.upper():
            return NodeRole.MASTER.value
        elif NodeRole.SLAVE.name in content.upper():
            return NodeRole.SLAVE.value
        else:
            return NodeRole.DATA.value

    @staticmethod
    def get_slave_ip_port(content):
        infos = content.split(',')
        salve_ip = None
        port = None
        for info in infos:
            if RedisNodeKeyConstant.IP.value in info:
                salve_ip = info.split('=')[-1]
            if RedisNodeKeyConstant.PORT.value in info:
                port = int(info.split('=')[-1])
        return salve_ip, port

    @staticmethod
    def convert_redis_info_to_dict(items):
        info = {}
        for item in items:
            contents = item.split(':')
            info[contents[0]] = contents[-1]
        return info

    @staticmethod
    def assert_empty_result(value):
        if not value:
            raise Exception(f'value is empty')

    @staticmethod
    def get_slot_info_from_cluster_nodes(info):
        # b74cf2ec ip:port@24352 master - 0 1657678844264 13 connected 0-1090 10922-12014 13107-16383
        # connected后面都是槽位号
        items = info.split(ClusterResManager.CONNECTED_FLAG)
        if len(items) > 1:
            return items[-1]
        return ''
